import wcwidth from "wcwidth";
import { AObject } from "./AObject";
import { AZone } from "./AZone";
import type { Character } from "./Character";
import * as Debug from "./Debug";
import { AHistory, ActionReportType, ContainerType, Direction, RoutineResult, StatusType } from "./Definitions";
import type { Pronoun } from "./NarrativeBase";
import { U } from "./Util";
import { World, type ScheduledProcess } from "./WorldBase";
import { RoutineSpecification, RoutineUtility } from "./ARoutine";

class GaitData {
    constructor(
        /** unit: m*s^-1 */
        public speed: number,
        /** unti: s^-1 (add random [0.00005, 0.0002]?) */
        public fatigue: number,
        public upSpeed: number,
        public upFatigue: number,
        public downSpeed: number,
        public downFatigue: number,
        public threshold: number) {}
}

export class Agent {
    constructor(public name: string, 
        public pronoun: Pronoun, 
        public parentZone: AZone) 
    {
        parentZone.agents.add(this);
        this._temperatureReference = parentZone.temperature;
    }

    inventory = new Set<AObject>;
    holdingItem: AObject | null = null;
    flags = new Set<string>;
    character: Character | null = null;
    memory = new AHistory(this);
    visited = new Set<AZone>;
    strength = 1;

    private _temperatureReference: number;

    attr = {
        walk: new GaitData(1.3, 0.00015, 1, 0.0005, 1.5, 0.0002, 0.05),
        run:  new GaitData(4  , 0.002  , 3, 0.01  , 3  , 0.003 , 0.15),
        /** unit: (kg*s)^-1 */
        weightPenalty: 0.0001,
        /** unit: s^-1 */
        standingRecover: 0.000,
        /** unit: s^-1 */
        restingRecover: 0.001,
        /** unit: cm^3 */
        inventoryMaxSize: 40000,
        /** unit: cm */
        handDiameter: 10,
    };

    /**
    * @returns Whether this area is lit, or the player has an activated light source
    * with them.
    */
    get hasLight() {
        if (this.parentZone.isLit) return true;
        for (let obj of this.inventory)
            if (obj.isLightSource) return true;
        return false;
    }

    get inventoryTotalSize() {
        let currentSize = 0;
        for (let x of this.inventory)
            currentSize += x.size;
        return currentSize;
    }

    get totalLoadMass() {
        let mass = this.holdingItem?.totalMass ?? 0;
        for (let o of this.inventory)
            mass += o.totalMass;
        return mass;
    }

    *healthCoroutine(): ScheduledProcess {
        while (true) {
            let target = World.schedule.absoluteTime + 1;
            while (true) {
                let dif = target - World.schedule.absoluteTime;
                if (dif < 0) break;
                let r = yield World.schedule.requireTime(dif);
                if (r.success) break;
            }
            this.strength += 
                this.attr.standingRecover - this.attr.weightPenalty * this.totalLoadMass;
            if (this.strength > 1) this.strength = 1;
            if (this.strength <= 0) {
                throw new Error('Not implemented');
            }
        }
    }

    readonly changeZoneInternal = 
    (function*(this: Agent, target: AZone, describe = true) {
        this.parentZone.agents.delete(this);
        target.agents.add(this);
        this.parentZone = target;

        // automatically describe new zone
        if (describe) {
            // ADV_TODO: tell Character to reflect on temperature

            // let temprep = Phrasebank.temperatureChange(
            //     target.temperature - this._temperatureReference);
            // if (temprep != null) {
            //     this._temperatureReference = target.temperature;
            //     U.passive(this, temprep).submit();
            // }
            Debug.assert(this.character != null);
            yield* this.character.describeZone();
            this.describeStatus(true);
        }
    }).bind(this);

    readonly addPossessionInternal = (obj: AObject) => {
        Debug.assert(obj.parentContainer == null);
        Debug.assert(obj.holder == null);

        console.log('adding possession of', obj.name.value);
        obj.parentContainer = this;
        if (obj.isHoldItem) {
            this.holdingItem = obj;
            obj.holder = this;
        } else {
            Debug.assert(this.inventoryTotalSize + obj.size < this.attr.inventoryMaxSize);
            Debug.assert(this.holdingItem == null);
            this.inventory.add(obj);
        }
    }

    readonly removePossessionInternal = (obj: AObject) => {
        if (obj.isHoldItem) {
            Debug.assert(this.holdingItem == obj && obj.holder == this);
            this.holdingItem = null;
            obj.holder = null;
        } else {
            Debug.assert(this.inventory.has(obj));
            this.inventory.delete(obj);
        }
        obj.parentContainer = null;
    }

    describeStatus(all = false) {
        // ADV_UNDONE
        let s = this.parentZone.status(this.parentZone, this);
        if (s != null && (s[1] != StatusType.Normal || all))
            U.metaText(this, 'status:' + s[0]);

        for (let obj of this.parentZone.extendedObjects()) {
            s = obj.status(obj, this);
            if (s != null && (s[1] != StatusType.Normal || all)) {
                U.metaText(this, s[0]);
            }
        }
    }
    
    describeObj = new RoutineSpecification<[AObject]>()
        .tryHandler((_, obj) => [obj, obj.beforeAction.Examine])
        .do((self, obj) => self.character.describeObject([obj]))
        .tryHandler((_, obj) => [obj, obj.afterAction.Examine])
        .ok();

    goDirection = new RoutineSpecification<[dir: Direction, run?: boolean]>()
        .fatigueAndTime((self, dir, run) => {
            let dest = self.agent.parentZone.passages(dir)!;
            let gait = run ? self.agent.attr.run : self.agent.attr.walk,
            speed = gait.speed, 
            fatrate = gait.fatigue;
            if (dir == Direction.Up)   speed = gait.upSpeed,   fatrate = gait.upFatigue;
            if (dir == Direction.Down) speed = gait.downSpeed, fatrate = gait.downFatigue;
            let time = dest[1] / speed, fatigue = time * fatrate;
            return [fatigue, time];
        })
        .report((self, dir, run) => ({
            type: run ? ActionReportType.RunTowards : ActionReportType.WalkTowards,
            zone: self.agent.parentZone.passages(dir)![0] }))
        // body
        .check((_1, _2, run = false) => !run,
               (self) => RoutineUtility.ensureNotHolding(self.agent))
        .do((self, dir, run = false) => self.agent.parentZone.beforeGoDirection ?
            self.agent.parentZone.beforeGoDirection(
                self.agent.parentZone, self.agent, dir, run) : null)
        .check((self, dir) => self.agent.parentZone.passages(dir) !== undefined,
               () => RoutineResult.NoPassage())
        .main(function*(self, dir) {
            let dest = self.agent.parentZone.passages(dir)!;
            yield* self.agent.changeZoneInternal(dest[0])
        }).ok();

    turnOnOff = new RoutineSpecification<[obj: AObject, state: boolean]>()
        .param('object', [(o) => o!.isSwitch, 
                          (o) => RoutineResult.ObjectNotSwitch({object: o})],
                         [(o, args) => o!.switchState != args.state, 
                          (o, args) => RoutineResult.ObjectAlreadyOnOff(
                                {object: o, state: args.state})])
        .checkParams((_, obj, state) => ({object: obj, state: state}))
        .fatigueAndTime([0.0002, 2])
        .report((_, obj, state) => ({
            type: ActionReportType.TurnOnOff, object: obj, state: state }))
        // body
        .ensureNotHolding()
        .tryHandler((_, obj, state) => 
            [obj, state ? obj.beforeAction.TurnOn : obj.beforeAction.TurnOff])
        .effect((_, obj, state) => {obj.switchState = state})
        .tryHandler((_, obj, state) => 
            [obj, state ? obj.afterAction.TurnOn : obj.afterAction.TurnOff])
        .ok();

    press = new RoutineSpecification<[obj: AObject]>()
        .param('object', [(obj) => obj!.customRoutines.Press != undefined,
                          () => RoutineResult.NotUseful()])
        .checkParams((_, obj) => ({object: obj}))
        .ensureNotHolding()
        .tryRoutine((_, obj) => [obj, obj.customRoutines.Press])
        .ok();

    push = new RoutineSpecification<[obj: AObject]>()
        .param('object', [(obj) => obj!.customRoutines.Push != undefined,
                          () => RoutineResult.NotUseful()])
        .checkParams((_, obj) => ({object: obj}))
        .ensureNotHolding()
        .tryRoutine((_, obj) => [obj, obj.customRoutines.Push])
        .ok();

    pull = new RoutineSpecification<[obj: AObject]>()
        .param('object', [(obj) => obj!.customRoutines.Pull != undefined,
                          () => RoutineResult.NotUseful()])
        .checkParams((_, obj) => ({object: obj}))
        .ensureNotHolding()
        .tryRoutine((_, obj) => [obj, obj.customRoutines.Pull])
        .ok();

    openClose = new RoutineSpecification<[obj: AObject, state: boolean]>()
        .param('object', [(o) => o!.isOpenable, 
                          (o) => RoutineResult.ObjectNotOpenable({object: o})],
                         [(o) => !o!.isLocked,
                          (o) => RoutineResult.ObjectLocked({object: o})],
                         [(o, args) => o!.openState != args.state, 
                          (o, args) => RoutineResult.ObjectAlreadyOpenedClosed(
                                {object: o, state: args.state})])
        .checkParams((_, obj, state) => ({object: obj, state: state}))
        .fatigueAndTime([0.0002, 2])
        .report((_, obj, state) => ({
            type: ActionReportType.OpenClose, object: obj, state: state }))
        // body
        .ensureNotHolding()
        .tryHandler((_, obj, state) => 
            [obj, state ? obj.beforeAction.Open : obj.beforeAction.Close])
        .effect((_, obj, state) => {
            obj.openState = state;
            let linked = obj.linkedSide(obj);
            if (linked)
                linked.openState = state;
        })
        .tryHandler((_, obj, state) => 
            [obj, state ? obj.afterAction.Open : obj.afterAction.Close])
        .ok();

    enterObj = new RoutineSpecification<[obj: AObject, run?: boolean]>()
        .param('object', [(o) => o!.isEnterable, 
                          (o) => RoutineResult.ObjectNotEnterable({object: o})])
        .checkParams((_, obj) => ({object: obj}))
        .fatigueAndTime((_, obj, run = false) => {
            let zoneTo = obj.zoneTo(obj);
            if (zoneTo == null) throw new Error(
                `No zoneTo in object ${obj.name}, which the agent tried to enter`);
            let gait = run ? this.attr.run : this.attr.walk;
            let time = zoneTo[1] / gait.speed,
                fatigue = time * gait.fatigue;
            return [fatigue, time];
        })
        .report((_, obj, run = false) => ({
            type: run ? ActionReportType.RunIntoObject 
                      : ActionReportType.WalkIntoObject, 
            object: obj, zone: obj.zoneTo(obj)![0] }))
        // body
        .do((_, obj) => RoutineUtility.ensureIsOpen(obj, this))
        .tryHandler((_, obj) => [obj, obj.beforeAction.Enter])
        .main((self, obj) => self.agent.changeZoneInternal(obj.zoneTo(obj)![0]))
        .tryHandler((_, obj) => [obj, obj.afterAction.Enter])
        .ok();

    takeObj = new RoutineSpecification<[obj: AObject]>()
        .param('object', [(o) => o!.isTakable, 
                          (o) => RoutineResult.ObjectNotTakable({object: o})],
                         [(o) => o!.parentContainer != this, 
                          (o) => RoutineResult.YouAlreadyHaveThis({object: o})])
        .checkParams((_, obj) => ({object: obj}))
        .fatigueAndTime((_, obj) => [
            this.attr.weightPenalty * obj.totalMass * 2,
            obj.isHoldItem ? 3 : 1.5])
        .report((_, obj) => ({
            type: obj.isHoldItem ? ActionReportType.HoldUpObject 
                                 : ActionReportType.PickUpObject,
            object: obj }))
        // body
        .ensureNotHolding()
        .tryHandler((_, obj) => [obj, obj.beforeAction.Take])
        .check((_, obj) => obj.isHoldItem || 
                    this.inventoryTotalSize + obj.size <= this.attr.inventoryMaxSize,
               (_, obj) => RoutineResult.InventoryFull({object: obj}, true))
        .main(function*(self, obj) {
            console.log('taking object', obj.name.value, 'from', obj.parentContainer?.name);
            if (obj.parentContainer instanceof AObject) {
                // try to remove object from its container
                let cont = obj.parentContainer;
                if (cont.mouthSize > 0 && cont.mouthSize < self.agent.attr.handDiameter)
                    return RoutineResult.OpeningTooNarrow({container: cont, object: obj});
                if (cont.isOpenable && !cont.openState) {
                    let ret = yield* self.agent.openClose.execIndirect(
                        self.agent, self.isSilent, cont, true);
                    if (ret.type) return ret;
                    if (!cont.openState)
                        return RoutineResult.CantDoWhileNotOpen({object: cont});
                }
                if (cont.beforeRemoveContent) {
                    let res = yield* cont.beforeRemoveContent(cont, self.agent, obj);
                    if (self.check(res)) return res;
                }
                let res = cont.RemoveContent(self.agent, obj);
                if (self.check(res)) return res;
                console.log('removed from container');
            } else if (obj.parentContainer instanceof AZone) {
                // try to remove object from its zone
                let res = obj.parentContainer.removeObject(self.agent, obj);
                if (self.check(res)) return res;
                console.log('removed from zone');
            } else if (obj.parentContainer instanceof Agent) {
                // try to remove object from its owner
                throw new Error('not implemented');
            }
            Debug.assert(obj.parentContainer == null);
            self.agent.addPossessionInternal(obj);
        })
        .tryHandler((_, obj) => [obj, obj.afterAction.Take])
        .ok();

    drop = new RoutineSpecification<[obj: AObject]>()
        .param('object', [(o) => o!.isChildrenOf(this), 
                          (o) => RoutineResult.YouDontHaveThis({object: o})])
        .checkParams((_, obj) => ({object: obj}))
        .fatigueAndTime((_, obj) => [this.attr.weightPenalty * obj.totalMass * 1.5, 1.5])
        .report((_, obj) => ({ type: ActionReportType.DropObject, object: obj }))
        // body
        .check((_, obj) => obj == this.holdingItem,
               () => RoutineUtility.ensureNotHolding(this))
        .tryHandler((_, obj) => [obj, obj.beforeAction.Drop])
        .do((_, obj) => RoutineUtility.ensureOwns(obj, this))
        .effect((_, obj) => {
            this.removePossessionInternal(obj);
            this.parentZone.addObject(this, obj);
        })
        .tryHandler((_, obj) => [obj, obj.afterAction.Drop])
        .ok();

    putObjIn = new RoutineSpecification<[obj: AObject, cont: AObject]>()
        .param('container', [(o) => o!.containerType != ContainerType.NotContainer, 
                             (o) => RoutineResult.ObjectNotContainer({object: o})])
        .param('object', [(o) => o!.isTakable, 
                          (o) => RoutineResult.ObjectNotTakable({object: o})],
                         [(o, arg) => o!.parentContainer != arg.container,
                          (o, arg) => RoutineResult.ObjectAlrealyInContainer(
                                {object: o, container: arg.container})],
                         [(o, arg) => o != arg.container, 
                          (o) => RoutineResult.CantPutObjectInItself({object: o})])
        .checkParams((_, obj, cont) => ({object: obj, container: cont}))
        .fatigueAndTime((_, obj) => [this.attr.weightPenalty * obj.totalMass * 2, 1.5])
        .report((_, obj, cont) => ({
            type: ActionReportType.PutInContainer, object: obj, container: cont }))
        // body
        // ensure possibility
        .then((_, obj, cont) => cont.AddContent(this, obj, true))
        .do((_, _2, cont) => RoutineUtility.ensureIsOpen(cont, this))
        .do((_, obj) => RoutineUtility.ensureOwns(obj, this))
        // confirmed 
        .do((_, obj, cont) => cont.beforeAddContent 
            ? cont.beforeAddContent(cont, this, obj) : null)
        .effect((_, obj, cont) => {
            this.removePossessionInternal(obj);
            let ret = cont.AddContent(this, obj, false);
            Debug.assert(ret == null);
        })
        .do((_, obj, cont) => cont.afterAddContent 
            ? cont.afterAddContent(cont, this, obj) : null)
        .ok();

    escape = new RoutineSpecification<[run?: boolean]>()
        // body
        .check((_, run = false) => !run, 
               () => RoutineUtility.ensureNotHolding(this))
        .do(function*(self, run) {
            let dir = self.agent.parentZone.exitDirection();
            if (dir != null) return yield* self.agent.goDirection.execIndirect(
                    self.agent, false, dir, run);
            let door = self.agent.parentZone.exitDoor();
            if (door != null) return yield* self.agent.enterObj.execIndirect(
                    self.agent, false, door, false);
            return RoutineResult.CantEscape();
        })
        .ok();

    listen = new RoutineSpecification<[]>()
        .fatigueAndTime([0, 1])
        .effect((self) => {
            let ret = this.parentZone.onListen(this.parentZone, this);
            if (ret != null) {
                U.meta(this, ret); // ADV_TODO
                return RoutineResult.Ok();
            }
            self.character.reportAction(
                { type: ActionReportType.HeardNothingSpecial }).submitMomentary();
        })
        .ok();
    
    listenTo = new RoutineSpecification<[obj: AObject]>()
        .fatigueAndTime([0, 1])
        .report(() => ({ type: ActionReportType.HeardNothingSpecial }))
        .tryRoutine((_, obj) => [obj, obj.customRoutines.Listen])
        .effect(() => {})
        .ok();
    

    smell = new RoutineSpecification<[]>()
        .fatigueAndTime([0, 1])
        .effect((self) => {
            let ret = this.parentZone.onSmell(this.parentZone, this);
            if (ret != null) {
                U.meta(this, ret); // ADV_TODO
                return RoutineResult.Ok();
            }
            self.character.reportAction(
                { type: ActionReportType.SmelledNothingSpecial }).submitMomentary();
        })
        .ok();
    
    smellAt = new RoutineSpecification<[obj: AObject]>()
        .fatigueAndTime([0, 1])
        .report(() => ({ type: ActionReportType.SmelledNothingSpecial }))
        .tryRoutine((_, obj) => [obj, obj.customRoutines.Smell])
        .effect(() => {})
        .ok();

    read = new RoutineSpecification<[obj: AObject]>()
        .param('object', [(o) => o!.writing.maxLength > 0 && o!.writing.text != '',
                          (o) => RoutineResult.ObjectNoWriting({object: o})])
        .checkParams((_, obj) => ({object: obj}))
        .do((self, obj) => self.character.readObject(obj))
        .ok();

    touch = new RoutineSpecification<[obj: AObject]>()
        .fatigueAndTime([0, 1.5])
        .report((_, obj) => ({ type: ActionReportType.TouchObject, object: obj }))
        // body
        .check((_, obj) => obj == this.holdingItem,
               () => RoutineUtility.ensureNotHolding(this))
        .tryRoutine((_, obj) => [obj, obj.customRoutines.Touch])
        .effect((self, obj) => {
            if (!self.isSilent) 
                self.character.reportAction({ type: ActionReportType.FeltNothingSpecial, object: obj }).submitMomentary();
        })
        .ok();

    shake = new RoutineSpecification<[obj: AObject]>()
        .fatigueAndTime((_, obj) => 
            [this.attr.weightPenalty * obj.totalMass * 1, obj.isHoldItem ? 3 : 1.5])
        .report((_, obj) => ({ type: ActionReportType.ShakeObject, object: obj }))
        // body
        .check((_, obj) => obj == this.holdingItem,
               () => RoutineUtility.ensureNotHolding(this))
        .tryRoutine((_, obj) => [obj, obj.customRoutines.Shake])
        .check((_, obj) => obj.isTakable,
               (_, obj) => RoutineResult.ObjectNotShakable({object: obj}))
        .effect(() => {})
        .ok();

    lockUnlock = new RoutineSpecification
            <[obj: AObject, state: boolean, key: AObject | null]>()
        .param('object', [(o, a) => (a.state ? o!.onLock : o!.onUnlock) != undefined,
                          (o, a) => RoutineResult.CantLockUnlockObject(
                                {object: o, state: a.state})],
                         [(o, a) => o!.isLocked != a.state,
                          (o, a) => RoutineResult.ObjectAlreadyLockedUnlocked(
                                {object: o, state: a.state})])
        .checkParams((_, obj, state) => ({object: obj, state: state}))
        .fatigueAndTime([0.001, 2.5])
        .report((_, obj, state, key) => ({
            type: ActionReportType.LockUnlockObject,
            state: state, object: obj, instrument: key ?? undefined }))
        // body
        .doIf((_, _1, _2, key) => key != null,
              (self, _, _2, k) => RoutineUtility.ensureOwns(k!, self.agent))
        .main(function*(self, obj, state, key) {
            let res = self.check(
                yield* (state ? obj.onLock : obj.onUnlock)!(obj, self.agent, key));
            if (res) return res;
            obj.isLocked = state;
            let link = obj.linkedSide(obj);
            if (link != null) link.isLocked = state;
        }).ok();

    write = new RoutineSpecification
            <[obj: AObject, instrument: AObject, text: string]>()
        .param('object', [(o) => o!.writing.maxLength > 0,
                          (o) => RoutineResult.ObjectNotWritable({object: o})])
        .param('instrument',
            [(o) => o!.writing.isInstrument,
             (o) => RoutineResult.ObjectNotWritingInstrument({object: o})])
        .checkParams((_, obj, instr) => ({object: obj, instrument: instr}))
        .report((_, obj, instr) => ({
            type: ActionReportType.WriteSomething, object: obj, instrument: instr }))
        // body
        .ensureNotHolding()
        .main(function*(self, obj, instr, text) {
            let len = wcwidth(obj.writing.text);
            let res = RoutineUtility.checkFatigue(self.agent, len * 0.0002);
            if (res) return res;
    
            for (let i = 0; i < text.length; i++) {
                let wid = wcwidth(text[i]);
                if (len + wid > obj.writing.maxLength)
                    return RoutineResult.NoWritingSpace({object: obj})
                obj.writing.text += text[i];
                let t = wid > 1 ? 0.8 : 0.15; // rough approx. for Chinese + English
                self.agent.strength -= wid * 0.0002;
                res = yield* RoutineUtility.requireTime(t, self.character);
                if (res) return res;
            }
        }).ok();

    erase = new RoutineSpecification<[obj: AObject, instrument: AObject]>()
        .param('object', [(o) => o!.writing.maxLength > 0 && o!.writing.text != '',
                          (o) => RoutineResult.NoWritingToErase({object: o})])
        .param('instrument', [(o) => o!.writing.isEraser,
                              (o) => RoutineResult.ObjectNotEraser({object: o})])
        .checkParams((_, obj, instr) => ({object: obj, instrument: instr}))
        .fatigueAndTime((_, obj) => {
            let width = wcwidth(obj.writing.text);
            return [width * 0.0001, Math.max(width * 0.05, 1.5)]; })
        .report((_, obj, instr) => ({
            type: ActionReportType.WriteSomething, object: obj, instrument: instr }))
        // body
        .ensureNotHolding()
        .do((_, _1, instr) => RoutineUtility.ensureOwns(instr, this))
        .effect((_, obj) => {obj.writing.text = ''})
        .ok();
}